/**
 * ...
 * @author 
 */

package scuts.ds;
import scuts.assert.Assert;
import scuts.functional.Tuples;

private typedef BaseHash<D,T> = {
    public function iterator() : Iterator<T>;
    public function keys() : Iterator<D>;
    public function remove( key : D ) : Bool;
    public function exists( key : D ) : Bool;
    public function get( key : D ) : Null<T>;
    public function set( key : D, value : T ) : Void;
}


private typedef Elem<T,D> = { key:T, value:D };

class DefaultHashMap<A,T,D> implements HashMap<T,D>
{
	var db:BaseHash<A, Array<Elem<T,D>>>;
	
	var equals: T->T->Bool;
	var hash: T->A;

    public var size(getSize, null):Int;
    
	
	var hasEquals:Bool;
	
	public function new(db: BaseHash<A, Array<Elem<T,D>>>, hash:T->A, ?equals:T->T->Bool) 
	{
		Assert.assertAllNotNull([db, hash]);
		
		this.db = db;
		this.size = 0;
		this.hash = hash;
		this.equals = equals;
		this.hasEquals = equals != null;
	}
    
	
	
	//{ region Private
	
	inline function elemsEquals (a:T, b:T):Bool 
	{
		return hasEquals ? equals(a, b) : (a == b);
	}
	
	function getSize ():Int
	{
		return size;
	}

	//} endregion
	
	public function set (key:T, value:D):Bool 
	{	
		Assert.assertNotNull(key);
		Assert.assertNotNull(value);
		
		var h:A = hash(key);
		
		var arr;
		
		if (!db.exists(h)) {
			arr = [];
			db.set(h, arr);
		} else {
			arr = db.get(h);
			// check if elem is already added
			for (i in arr) {
				if (elemsEquals (i.key, key)) return false;
			}
		}
		++size;
		arr.push( { key: key, value:value } );
		return true;
	}
	
	public function remove (key:T):Null<D> 
	{
		
		Assert.assertNotNull(key);
		
		var h:A = hash(key);
		
		var arr = db.get(h);
		
		if (arr == null) return null;
		
		
		for (i in arr) {
			if ( elemsEquals( i.key, key ) ) {
				arr.remove(i);
				--size;
				if (arr.length == 0) {
					db.remove(h);
				}
				
				return i.value;
			}
		}
		
		return null;
		
	}
	
	public function get (key:T):Null<D>
	{
		Assert.assertNotNull(key);
		
		var h:A = hash(key);
		
		var arr = db.get(h);
		
		if (arr == null) return null;
		
		for (i in arr) 
		{
			if ( elemsEquals( i.key, key ) ) 
			{
				return i.value;
			}
		}
		
		return null;

	}
    
	public function exists (key:T):Bool 
	{
		Assert.assertNotNull(key);
		
		var h:A = hash(key);
		
		var arr = db.get(h);
		
		if (arr == null) return false;
		
		for (i in arr) 
		{
			if ( elemsEquals (i.key, key) ) return true;
		}
		
		return false;
		
	}
	
	public function keys ():Iterable<T>
    {
        var all= [];
		
		for (i in db) {
			for (j in i) {
				all.push(j.key);
			}
		}
		return all;
    }
    
    public function elems ():Iterable<D>
	{
		var all = [];
		
		for (i in db) {
			for (j in i) {
				all.push(j.value);
			}
		}
		return all;
	}
	
    public function pairs ():Iterable<Tup2<T,D>>
	{
		var all= [];
		
		for (i in db) {
			for (j in i) {
				all.push(Tuples.mk(j.key, j.value));
			}
		}
		return all;
	}
    
	public inline function iterator ():Iterator<D>
	{
		return elems().iterator();
	}
	
	public function removeAll ():Void
	{   
		for (i in db) {
			while (i.length > 0) {
				i.pop();
			}
		}
		for (i in db.keys()) {
			db.remove(i);
		}
		
		
	}
}